package com.medical.service.impl;

import com.medical.constants.CacheConstants;
import com.medical.enums.TimeEnum;
import com.medical.mapper.ChatMessagesMapper;
import com.medical.mapper.KnowledgeMapper;
import com.medical.model.entity.Knowledge;
import com.medical.model.param.ChatParameters;
import com.medical.model.param.Messages;
import com.medical.service.ILLMService;
import com.medical.service.RedisService;
import com.medical.utils.ChatUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author TISNIW
 * @version 1.0
 * @date 2025/6/16 11:12
 * @desc
 */
@Slf4j
@Service
public class LLMServiceImpl implements ILLMService {

    @Autowired
    private RedisService redisService;

    @Autowired
    private ChatUtil chatUtil;

    @Autowired
    private ChatMessagesMapper messagesMapper;
    @Autowired
    private KnowledgeMapper knowledgeMapper;

    @Override
    public String chat(String model, String prompt, String sessionId, Boolean think) {
        // 1. 构建系统提示词
        Messages systemMessage = Messages.builder()
                .role("system")
                .content("你是一个专业的医疗智能助手，你的职责是根据用户提供的健康相关问题及相关的医学知识，提供科学、准确、可理解的医疗建议。\n" +
                        "\n" +
                        "### 回答规则：\n" +
                        "1. 所有回答必须使用中文，采用 Markdown 格式进行排版。\n" +
                        "2. 回答必须按点列出，结构清晰，便于阅读。\n" +
                        "3. **重要**：你需要基于用户提供的'知识'部分来回答问题。请仔细分析这部分信息，并将其融入到你的回答中。\n" +
                        "4. 每次回答前，你需要先进行内部思考，将复杂问题拆分为多个小问题，逐个分析后再组织答案。\n" +
                        "5. 不得忽略用户的任何信息，包括症状描述、持续时间、既往病史等。\n" +
                        "6. 对于不确定的问题，应说明当前医学认知范围，并建议及时就医或咨询专业医生。\n" +
                        "7. 不得提供诊断、处方或治疗建议，仅限科普性质的内容和初步建议。\n" +
                        "8. 若涉及紧急情况（如胸痛、呼吸困难、意识丧失等），需立即提示用户寻求急救服务。\n" +
                        "9. 禁止使用任何形式的广告、营销信息、法律条款、隐私政策或其他法律相关内容。\n" +
                        "10. 如果包含知识就必须严格按照知识进行回答，不能回答额外内容。\n" +
                        "\n" +
                        "### 输出格式要求：\n" +
                        "- 使用 `## 思考过程` ,##获取知识重述,`## 问题拆分`, `## 回答`,`## 建议` 分段展示内容。\n" +
                        "- '知识'部分应当被充分引用并解释在回答中。\n" +
                        "- 思考过程部分不对外显示，仅供内部逻辑梳理。\n" +
                        "- 回答部分应条理清晰，适当使用标题、列表、加粗等方式提升可读性。\n" +
                        "\n" +
                        "请始终遵循以上规则，确保输出内容专业、安全、有用。")
                .build();

        // 2. 获取相似知识库内容
        List<Knowledge> similarKnowledges = retrieveSimilarKnowledge(prompt);
        StringBuilder similarContent = new StringBuilder();
        for (Knowledge similarKnowledge : similarKnowledges) {
            similarContent.append(similarKnowledge.getTitle() + "->" + similarKnowledge.getContent()).append("\n");
            log.info("相似知识库内容：{}", similarKnowledge.getContent());
        }

        // 3. 获取或创建会话参数
        ChatParameters parameters = redisService.getCacheObject(CacheConstants.AI_CHAT_SESSION_KEY + sessionId, ChatParameters.class);
        List<Messages> historyMessages;

        if (parameters == null) {
            // 如果缓存不存在，则从数据库加载历史记录
            historyMessages = messagesMapper.getChatParameters(sessionId);
            if (historyMessages == null || historyMessages.isEmpty()) {
                historyMessages = new ArrayList<>();
            }
        } else {
            historyMessages = parameters.getMessages();
        }

        // 4. 截取最近 5 轮对话（user + assistant）
        List<Messages> limitedHistory = getLimitedHistory(historyMessages, 5);

        // 5. 添加 system message 并插入当前用户输入
        List<Messages> context = new ArrayList<>();
        context.add(systemMessage); // 系统指令永远在最前面
        context.addAll(limitedHistory); // 添加截断后的上下文
        context.add(Messages.builder().role("user").content(prompt).build()); // 添加当前用户输入

        // 6. 更新 ChatParameters 并写入缓存
        ChatParameters newParameters = ChatParameters.builder()
                .model(model)
                .messages(context)
                .think(think)
                .stream(true)
                .build();
        redisService.setCacheObject(CacheConstants.AI_CHAT_SESSION_KEY + sessionId, newParameters, TimeEnum.THIRTY_MINUTE);

        // 7. 存储用户原始问题到数据库
        messagesMapper.insertMessage(sessionId, "user", prompt);

        // 8. 重构用户问题（加入知识）
        String refinedPrompt =" 使用 `## 思考过程` ,`## 问题拆分`, `## 回答`,`## 建议` 分段展示内容。如果包含知识必须严格按照知识进行回答，同时将知识进行重述"+ "问题:" + prompt + " 知识:" + similarContent.toString();
        log.info("refinedPrompt: {}", refinedPrompt);
        log.info("上下文列表长度:{}" , context.size());

        // 9. 调用流式回复
        chatUtil.chatStream(model, refinedPrompt, context, sessionId, think);

        return sessionId;
    }
    /**
     * 获取最近 N 轮完整的对话交互（user + assistant）
     *
     * @param messages 完整的历史消息列表
     * @param rounds   需要保留的完整对话轮数
     * @return 截断后的消息列表
     */
    private List<Messages> getLimitedHistory(List<Messages> messages, int rounds) {
        List<Messages> result = new ArrayList<>();
        int count = 0;

        // 从后往前遍历，优先保留最近的完整对话
        for (int i = messages.size() - 1; i >= 0; i--) {
            Messages msg = messages.get(i);
            if ("user".equals(msg.getRole()) || "assistant".equals(msg.getRole())) {
                result.add(0, msg); // 插入到开头，保证顺序不变
                count++;
                if (count >= rounds * 2) { // 每轮包含 user + assistant
                    break;
                }
            }
        }

        return result;
    }

    /**
     * 获取会话中的消息记录
     *
     * @return 消息列表
     */
    @Override
    public List<Messages> getMessages(String id) {
        log.info(id);
        return messagesMapper.getChatParameters(id);
    }
    public List<Knowledge> retrieveSimilarKnowledge(String query) {
        return chatUtil.retrieveSimilarKnowledge(query);
    }

    // 计算余弦相似度的方法
    private double computeCosineSimilarity(float[] vecA, float[] vecB) {
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        for (int i = 0; i < vecA.length; i++) {
            dotProduct += vecA[i] * vecB[i];
            normA += Math.pow(vecA[i], 2);
            normB += Math.pow(vecB[i], 2);
        }
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}